Skip to content

fix(pg): Inconsistent conditional pattern matching BED-6695#73

Open
seanjSO wants to merge 7 commits into
mainfrom
seanj/BED-6695
Open

fix(pg): Inconsistent conditional pattern matching BED-6695#73
seanjSO wants to merge 7 commits into
mainfrom
seanj/BED-6695

Conversation

@seanjSO
Copy link
Copy Markdown
Contributor

@seanjSO seanjSO commented May 7, 2026

Description

Resolves: BED-6695

Type of Change

  • Chore (a change that does not modify the application functionality)
  • Bug fix (a change that fixes an issue)
  • New feature / enhancement (a change that adds new functionality)
  • Refactor (no behaviour change)
  • Test coverage
  • Build / CI / tooling
  • Documentation

Testing

  • Unit tests added / updated
  • Integration tests added / updated
  • Manual integration tests run (go test -tags manual_integration ./integration/...)

Screenshots (if appropriate):

Driver Impact

  • PostgreSQL driver (drivers/pg)
  • Neo4j driver (drivers/neo4j)

Checklist

  • Code is formatted
  • All existing tests pass
  • go.mod / go.sum are up to date if dependencies changed

Summary by CodeRabbit

  • Bug Fixes

    • Improved SQL translation for Cypher queries containing negated relationship patterns and pattern predicates.
    • Enhanced handling of bidirectional pattern matching with optimized query correlation.
  • Tests

    • Added comprehensive integration test suite validating pattern predicate behavior across multiple query scenarios.

Review Change Stack

@seanjSO seanjSO self-assigned this May 7, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR refactors pattern predicate translation for Cypher queries by introducing outer-correlated traversal root builders. The changes branch join/WHERE logic based on which nodes are pre-bound in the outer query, then update SQL test expectations and add integration fixtures to validate pattern-existence semantics for negated and conditional patterns.

Changes

Pattern Predicate Outer-Correlated Translation

Layer / File(s) Summary
Outer-correlated traversal root implementation
cypher/models/pgsql/translate/traversal.go
Adjusts right-node constraint in buildDirectionlessTraversalPatternRoot via OptionalAnd; introduces buildTraversalPatternRootWithOuterCorrelation to dispatch different FROM/JOIN/WHERE structures based on left/right node binding state.
Optimized predicate and traversal selection
cypher/models/pgsql/translate/predicate.go
Branches buildOptimizedRelationshipExistPredicate to generate context-specific start/end predicates; updates buildPatternPredicates to invoke outer-correlated root when either endpoint is pre-bound; adds inline documentation.
SQL translation test expectations
cypher/models/pgsql/test/translation_cases/nodes.sql, cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql
Regenerates SQL for negated relationship patterns, constrained directional cases, and stepwise traversals; reflects bind/WHERE restructuring to move outer node-id equality checks into CTE WHERE clauses and bind via direct edge→node joins.
Pattern predicate integration fixtures
integration/testdata/bed6695.json, integration/testdata/cases/bed6695-pattern_predicates.json
Adds BED-6695 graph with key-admin, member, user, and group nodes and inter-node edges; adds 12 pattern-predicate test cases validating negation, directionality equivalence, property filters, and both-bound node pair validation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 The rabbits rejoice at predicates bound,
Where outer scope lingers all around,
No join-clause confusion, just WHERE-clause grace,
Pattern roots dancing through traversal space!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(pg): Inconsistent conditional pattern matching BED-6695' directly reflects the main change: fixing inconsistent conditional pattern matching in PostgreSQL, with specific reference to issue BED-6695.
Description check ✅ Passed The pull request description follows the template structure, includes the issue reference (BED-6695), marks appropriate change type and testing checkboxes, and specifies PostgreSQL driver impact.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch seanj/BED-6695

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@seanjSO seanjSO added bug Something isn't working go Pull requests that update go code labels May 7, 2026
@seanjSO seanjSO changed the title BED-6695: Inconsistent conditional pattern matching fix(pg): Inconsistent conditional pattern matching BED-6695 May 7, 2026
@seanjSO seanjSO force-pushed the seanj/BED-6695 branch 2 times, most recently from 4894815 to 24385cb Compare May 12, 2026 15:55
@seanjSO seanjSO marked this pull request as ready for review May 12, 2026 15:56
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
integration/testdata/cases/pattern_predicate_direction_inline.json (1)

1-131: ⚡ Quick win

Consider extracting common fixture data to reduce duplication.

All six test cases define nearly identical inline fixtures (nodes u1, u2, g1, g2, g3, g4 and their edges). While this makes each test self-contained, it creates a maintenance burden—any fixture adjustment requires updating six locations.

If these fixtures need to remain inline, consider at least documenting why the duplication is intentional (e.g., for regression test isolation).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integration/testdata/cases/pattern_predicate_direction_inline.json` around
lines 1 - 131, The fixtures for the six cases (e.g., cases named "regression:
incoming negated pattern with contains predicate (left-directed form)",
"right-directed equivalent", etc.) duplicate the same nodes and edges; extract
that shared nodes/edges JSON into a single reusable fixture object (or helper
function) and reference it from each case to avoid repetition, or if inline
fixtures must remain, add a short explanatory comment in each case noting the
intentional duplication for regression/isolation; update the cases that
reference the repeated nodes (u1,u2,g1,g2,g3,g4) and edges (EdgeKind1/EdgeKind2
entries) to use the shared fixture identifier or include the comment.
cypher/models/pgsql/test/pattern_predicate_shape_test.go (2)

38-40: 💤 Low value

Add a comment explaining why this pattern is forbidden.

The forbidden fragment "from s0 join edge e0 on (s0.n0).id = e0.end_id" represents the buggy SQL structure that this fix addresses, but the reason isn't documented. A brief comment would help future maintainers understand the regression being prevented.

📝 Suggested comment
 	forbiddenFragments := []string{
+		// This join pattern caused incorrect correlation in negated pattern predicates (BED-6695).
+		// The fix restructures the CTE to avoid binding the outer reference too early.
 		"from s0 join edge e0 on (s0.n0).id = e0.end_id",
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cypher/models/pgsql/test/pattern_predicate_shape_test.go` around lines 38 -
40, Add a short explanatory comment above the forbiddenFragments entry for the
string "from s0 join edge e0 on (s0.n0).id = e0.end_id" stating that this SQL
pattern is forbidden because it uses a nested node accessor `(s0.n0).id` in the
JOIN ON clause (producing incorrect/buggy SQL generation or runtime errors for
nullable/composite node columns) and that the test prevents regressions related
to that specific buggy join shape; update the comment to reference the exact
fragment and the regression it guards against so future maintainers understand
why it must remain forbidden.

24-30: 💤 Low value

Consider deriving kind IDs dynamically from test constants.

The hard-coded array values [1] and [3] in the SQL assertions must match NodeKind1 and EdgeKind1 as mapped by newKindMapper(). If the kind mapper changes, these assertions could silently pass with incorrect SQL.

♻️ Alternative: compute expected values from the mapper

You could call newKindMapper() and extract the mapped IDs to build the expected fragments dynamically. However, since this is a focused regression test for a specific bug fix and the mapper is stable test infrastructure, the current approach may be acceptable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cypher/models/pgsql/test/pattern_predicate_shape_test.go` around lines 24 -
30, The test currently hardcodes kind ID arrays in requiredFragments which can
drift from the mapping; obtain the mapper via newKindMapper(), use it to look up
the mapped IDs for NodeKind1 and EdgeKind1, and construct the expected SQL
fragments using those computed IDs instead of literal "[1]" and "[3]"; update
the requiredFragments construction (the variable named requiredFragments in the
test) to interpolate the derived values so assertions remain correct if
newKindMapper() changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@cypher/models/pgsql/test/pattern_predicate_shape_test.go`:
- Around line 38-40: Add a short explanatory comment above the
forbiddenFragments entry for the string "from s0 join edge e0 on (s0.n0).id =
e0.end_id" stating that this SQL pattern is forbidden because it uses a nested
node accessor `(s0.n0).id` in the JOIN ON clause (producing incorrect/buggy SQL
generation or runtime errors for nullable/composite node columns) and that the
test prevents regressions related to that specific buggy join shape; update the
comment to reference the exact fragment and the regression it guards against so
future maintainers understand why it must remain forbidden.
- Around line 24-30: The test currently hardcodes kind ID arrays in
requiredFragments which can drift from the mapping; obtain the mapper via
newKindMapper(), use it to look up the mapped IDs for NodeKind1 and EdgeKind1,
and construct the expected SQL fragments using those computed IDs instead of
literal "[1]" and "[3]"; update the requiredFragments construction (the variable
named requiredFragments in the test) to interpolate the derived values so
assertions remain correct if newKindMapper() changes.

In `@integration/testdata/cases/pattern_predicate_direction_inline.json`:
- Around line 1-131: The fixtures for the six cases (e.g., cases named
"regression: incoming negated pattern with contains predicate (left-directed
form)", "right-directed equivalent", etc.) duplicate the same nodes and edges;
extract that shared nodes/edges JSON into a single reusable fixture object (or
helper function) and reference it from each case to avoid repetition, or if
inline fixtures must remain, add a short explanatory comment in each case noting
the intentional duplication for regression/isolation; update the cases that
reference the repeated nodes (u1,u2,g1,g2,g3,g4) and edges (EdgeKind1/EdgeKind2
entries) to use the shared fixture identifier or include the comment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 50427ad2-cb7e-47a1-8ffb-cd7db5659309

📥 Commits

Reviewing files that changed from the base of the PR and between ae5d313 and 24385cb.

📒 Files selected for processing (9)
  • cypher/models/pgsql/test/pattern_predicate_shape_test.go
  • cypher/models/pgsql/test/translation_cases/nodes.sql
  • cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql
  • cypher/models/pgsql/translate/predicate.go
  • cypher/models/pgsql/translate/traversal.go
  • integration/testdata/bed6695.json
  • integration/testdata/cases/pattern_predicate_direction_inline.json
  • integration/testdata/cases/pattern_predicates.json
  • integration/testdata/empty.json

@zinic
Copy link
Copy Markdown
Contributor

zinic commented May 12, 2026

Change appears to cover the current issue identified in the ticket. I noticed however that this missed the undirected traversal path. Checking with local tests and tooling, the defect does still exist in undirected path predicate translation:

match (g:NodeKind2) where g.name starts with 'KEY ADMINS' and not ((g)-[:EdgeKind1]-(:NodeKind1)) return count(g)

I don't have a problem with deferring the fix above for undirected path predicates to a different ticket but I felt it worth bringing up. Everything else in this changeset passes muster.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cypher/models/pgsql/translate/predicate.go (1)

154-179: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep processing later pattern predicates after an optimized hit.

Line 178 exits buildPatternPredicates from inside the loop. If the same WHERE has another deferred pattern predicate after this one, it never gets resolved. This should continue, not return.

Suggested fix
-					return nil
+					continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cypher/models/pgsql/translate/predicate.go` around lines 154 - 179, In
buildPatternPredicates, when an optimized relationship-exists predicate is
applied for a single-step traversal (the block that calls
buildOptimizedRelationshipExistPredicate and sets predicateFuture.SyntaxNode),
do not return nil immediately; replace the early return with a continue so the
loop keeps processing subsequent pattern predicates in the same WHERE; ensure
the same traversalStep, traversalStepIdentifiers and predicateFuture handling
remains unchanged and only the control flow is modified.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cypher/models/pgsql/translate/predicate.go`:
- Around line 75-90: The current fallback branch still references
traversalStep.LeftNode.Identifier even when both LeftNodeBound and
RightNodeBound are false, producing invalid SQL for a fully-unbound pattern; add
an explicit branch that detects (!traversalStep.LeftNodeBound &&
!traversalStep.RightNodeBound) and for that case set whereClause to a simple
edge-existence predicate (e.g., an IS NOT NULL or existence check on
traversalStep.Edge.Identifier/its primary id) or omit the predicate entirely
instead of comparing to traversalStep.LeftNode.Identifier.id; update the logic
around whereClause construction (the block using pgsql.NewBinaryExpression with
ColumnStartID/ColumnEndID and ColumnID) to only run when at least one side is
bound.

---

Outside diff comments:
In `@cypher/models/pgsql/translate/predicate.go`:
- Around line 154-179: In buildPatternPredicates, when an optimized
relationship-exists predicate is applied for a single-step traversal (the block
that calls buildOptimizedRelationshipExistPredicate and sets
predicateFuture.SyntaxNode), do not return nil immediately; replace the early
return with a continue so the loop keeps processing subsequent pattern
predicates in the same WHERE; ensure the same traversalStep,
traversalStepIdentifiers and predicateFuture handling remains unchanged and only
the control flow is modified.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a57d3c2d-4793-474f-8322-c533de8e9c46

📥 Commits

Reviewing files that changed from the base of the PR and between 88b2098 and 856f971.

📒 Files selected for processing (3)
  • cypher/models/pgsql/test/translation_cases/pattern_binding.sql
  • cypher/models/pgsql/translate/predicate.go
  • cypher/models/pgsql/translate/traversal.go

Comment thread cypher/models/pgsql/translate/predicate.go Outdated
@seanjSO seanjSO force-pushed the seanj/BED-6695 branch 2 times, most recently from 5fa1740 to a5c8d42 Compare May 18, 2026 18:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working go Pull requests that update go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants